( function () {
class RenderableObject {
  constructor() {
    this.id = 0;
    this.object = null;
    this.z = 0;
    this.renderOrder = 0;
  }

} //


class RenderableFace {
  constructor() {
    this.id = 0;
    this.v1 = new RenderableVertex();
    this.v2 = new RenderableVertex();
    this.v3 = new RenderableVertex();
    this.normalModel = new THREE.Vector3();
    this.vertexNormalsModel = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()];
    this.vertexNormalsLength = 0;
    this.color = new THREE.Color();
    this.material = null;
    this.uvs = [new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2()];
    this.z = 0;
    this.renderOrder = 0;
  }

} //


class RenderableVertex {
  constructor() {
    this.position = new THREE.Vector3();
    this.positionWorld = new THREE.Vector3();
    this.positionScreen = new THREE.Vector4();
    this.visible = true;
  }

  copy(vertex) {
    this.positionWorld.copy(vertex.positionWorld);
    this.positionScreen.copy(vertex.positionScreen);
  }

} //


class RenderableLine {
  constructor() {
    this.id = 0;
    this.v1 = new RenderableVertex();
    this.v2 = new RenderableVertex();
    this.vertexColors = [new THREE.Color(), new THREE.Color()];
    this.material = null;
    this.z = 0;
    this.renderOrder = 0;
  }

} //


class RenderableSprite {
  constructor() {
    this.id = 0;
    this.object = null;
    this.x = 0;
    this.y = 0;
    this.z = 0;
    this.rotation = 0;
    this.scale = new THREE.Vector2();
    this.material = null;
    this.renderOrder = 0;
  }

} //


class Projector {
  constructor() {
    let _object,
        _objectCount,
        _objectPoolLength = 0,
        _vertex,
        _vertexCount,
        _vertexPoolLength = 0,
        _face,
        _faceCount,
        _facePoolLength = 0,
        _line,
        _lineCount,
        _linePoolLength = 0,
        _sprite,
        _spriteCount,
        _spritePoolLength = 0,
        _modelMatrix;

    const _renderData = {
      objects: [],
      lights: [],
      elements: []
    },
          _vector3 = new THREE.Vector3(),
          _vector4 = new THREE.Vector4(),
          _clipBox = new THREE.Box3(new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1)),
          _boundingBox = new THREE.Box3(),
          _points3 = new Array(3),
          _viewMatrix = new THREE.Matrix4(),
          _viewProjectionMatrix = new THREE.Matrix4(),
          _modelViewProjectionMatrix = new THREE.Matrix4(),
          _frustum = new THREE.Frustum(),
          _objectPool = [],
          _vertexPool = [],
          _facePool = [],
          _linePool = [],
          _spritePool = []; //


    this.projectVector = function (vector, camera) {
      console.warn('THREE.Projector: .projectVector() is now vector.project().');
      vector.project(camera);
    };

    this.unprojectVector = function (vector, camera) {
      console.warn('THREE.Projector: .unprojectVector() is now vector.unproject().');
      vector.unproject(camera);
    };

    this.pickingRay = function () {
      console.error('THREE.Projector: .pickingRay() is now raycaster.setFromCamera().');
    }; //


    function RenderList() {
      const normals = [];
      const colors = [];
      const uvs = [];
      let object = null;
      const normalMatrix = new THREE.Matrix3();

      function setObject(value) {
        object = value;
        normalMatrix.getNormalMatrix(object.matrixWorld);
        normals.length = 0;
        colors.length = 0;
        uvs.length = 0;
      }

      function projectVertex(vertex) {
        const position = vertex.position;
        const positionWorld = vertex.positionWorld;
        const positionScreen = vertex.positionScreen;
        positionWorld.copy(position).applyMatrix4(_modelMatrix);
        positionScreen.copy(positionWorld).applyMatrix4(_viewProjectionMatrix);
        const invW = 1 / positionScreen.w;
        positionScreen.x *= invW;
        positionScreen.y *= invW;
        positionScreen.z *= invW;
        vertex.visible = positionScreen.x >= -1 && positionScreen.x <= 1 && positionScreen.y >= -1 && positionScreen.y <= 1 && positionScreen.z >= -1 && positionScreen.z <= 1;
      }

      function pushVertex(x, y, z) {
        _vertex = getNextVertexInPool();

        _vertex.position.set(x, y, z);

        projectVertex(_vertex);
      }

      function pushNormal(x, y, z) {
        normals.push(x, y, z);
      }

      function pushColor(r, g, b) {
        colors.push(r, g, b);
      }

      function pushUv(x, y) {
        uvs.push(x, y);
      }

      function checkTriangleVisibility(v1, v2, v3) {
        if (v1.visible === true || v2.visible === true || v3.visible === true) return true;
        _points3[0] = v1.positionScreen;
        _points3[1] = v2.positionScreen;
        _points3[2] = v3.positionScreen;
        return _clipBox.intersectsBox(_boundingBox.setFromPoints(_points3));
      }

      function checkBackfaceCulling(v1, v2, v3) {
        return (v3.positionScreen.x - v1.positionScreen.x) * (v2.positionScreen.y - v1.positionScreen.y) - (v3.positionScreen.y - v1.positionScreen.y) * (v2.positionScreen.x - v1.positionScreen.x) < 0;
      }

      function pushLine(a, b) {
        const v1 = _vertexPool[a];
        const v2 = _vertexPool[b]; // Clip

        v1.positionScreen.copy(v1.position).applyMatrix4(_modelViewProjectionMatrix);
        v2.positionScreen.copy(v2.position).applyMatrix4(_modelViewProjectionMatrix);

        if (clipLine(v1.positionScreen, v2.positionScreen) === true) {
          // Perform the perspective divide
          v1.positionScreen.multiplyScalar(1 / v1.positionScreen.w);
          v2.positionScreen.multiplyScalar(1 / v2.positionScreen.w);
          _line = getNextLineInPool();
          _line.id = object.id;

          _line.v1.copy(v1);

          _line.v2.copy(v2);

          _line.z = Math.max(v1.positionScreen.z, v2.positionScreen.z);
          _line.renderOrder = object.renderOrder;
          _line.material = object.material;

          if (object.material.vertexColors) {
            _line.vertexColors[0].fromArray(colors, a * 3);

            _line.vertexColors[1].fromArray(colors, b * 3);
          }

          _renderData.elements.push(_line);
        }
      }

      function pushTriangle(a, b, c, material) {
        const v1 = _vertexPool[a];
        const v2 = _vertexPool[b];
        const v3 = _vertexPool[c];
        if (checkTriangleVisibility(v1, v2, v3) === false) return;

        if (material.side === THREE.DoubleSide || checkBackfaceCulling(v1, v2, v3) === true) {
          _face = getNextFaceInPool();
          _face.id = object.id;

          _face.v1.copy(v1);

          _face.v2.copy(v2);

          _face.v3.copy(v3);

          _face.z = (v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z) / 3;
          _face.renderOrder = object.renderOrder; // face normal

          _vector3.subVectors(v3.position, v2.position);

          _vector4.subVectors(v1.position, v2.position);

          _vector3.cross(_vector4);

          _face.normalModel.copy(_vector3);

          _face.normalModel.applyMatrix3(normalMatrix).normalize();

          for (let i = 0; i < 3; i++) {
            const normal = _face.vertexNormalsModel[i];
            normal.fromArray(normals, arguments[i] * 3);
            normal.applyMatrix3(normalMatrix).normalize();
            const uv = _face.uvs[i];
            uv.fromArray(uvs, arguments[i] * 2);
          }

          _face.vertexNormalsLength = 3;
          _face.material = material;

          if (material.vertexColors) {
            _face.color.fromArray(colors, a * 3);
          }

          _renderData.elements.push(_face);
        }
      }

      return {
        setObject: setObject,
        projectVertex: projectVertex,
        checkTriangleVisibility: checkTriangleVisibility,
        checkBackfaceCulling: checkBackfaceCulling,
        pushVertex: pushVertex,
        pushNormal: pushNormal,
        pushColor: pushColor,
        pushUv: pushUv,
        pushLine: pushLine,
        pushTriangle: pushTriangle
      };
    }

    const renderList = new RenderList();

    function projectObject(object) {
      if (object.visible === false) return;

      if (object.isLight) {
        _renderData.lights.push(object);
      } else if (object.isMesh || object.isLine || object.isPoints) {
        if (object.material.visible === false) return;
        if (object.frustumCulled === true && _frustum.intersectsObject(object) === false) return;
        addObject(object);
      } else if (object.isSprite) {
        if (object.material.visible === false) return;
        if (object.frustumCulled === true && _frustum.intersectsSprite(object) === false) return;
        addObject(object);
      }

      const children = object.children;

      for (let i = 0, l = children.length; i < l; i++) {
        projectObject(children[i]);
      }
    }

    function addObject(object) {
      _object = getNextObjectInPool();
      _object.id = object.id;
      _object.object = object;

      _vector3.setFromMatrixPosition(object.matrixWorld);

      _vector3.applyMatrix4(_viewProjectionMatrix);

      _object.z = _vector3.z;
      _object.renderOrder = object.renderOrder;

      _renderData.objects.push(_object);
    }

    this.projectScene = function (scene, camera, sortObjects, sortElements) {
      _faceCount = 0;
      _lineCount = 0;
      _spriteCount = 0;
      _renderData.elements.length = 0;
      if (scene.autoUpdate === true) scene.updateMatrixWorld();
      if (camera.parent === null) camera.updateMatrixWorld();

      _viewMatrix.copy(camera.matrixWorldInverse);

      _viewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, _viewMatrix);

      _frustum.setFromProjectionMatrix(_viewProjectionMatrix); //


      _objectCount = 0;
      _renderData.objects.length = 0;
      _renderData.lights.length = 0;
      projectObject(scene);

      if (sortObjects === true) {
        _renderData.objects.sort(painterSort);
      } //


      const objects = _renderData.objects;

      for (let o = 0, ol = objects.length; o < ol; o++) {
        const object = objects[o].object;
        const geometry = object.geometry;
        renderList.setObject(object);
        _modelMatrix = object.matrixWorld;
        _vertexCount = 0;

        if (object.isMesh) {
          if (geometry.isBufferGeometry) {
            let material = object.material;
            const isMultiMaterial = Array.isArray(material);
            const attributes = geometry.attributes;
            const groups = geometry.groups;
            if (attributes.position === undefined) continue;
            const positions = attributes.position.array;

            for (let i = 0, l = positions.length; i < l; i += 3) {
              let x = positions[i];
              let y = positions[i + 1];
              let z = positions[i + 2];

              if (material.morphTargets === true) {
                const morphTargets = geometry.morphAttributes.position;
                const morphTargetsRelative = geometry.morphTargetsRelative;
                const morphInfluences = object.morphTargetInfluences;

                for (let t = 0, tl = morphTargets.length; t < tl; t++) {
                  const influence = morphInfluences[t];
                  if (influence === 0) continue;
                  const target = morphTargets[t];

                  if (morphTargetsRelative) {
                    x += target.getX(i / 3) * influence;
                    y += target.getY(i / 3) * influence;
                    z += target.getZ(i / 3) * influence;
                  } else {
                    x += (target.getX(i / 3) - positions[i]) * influence;
                    y += (target.getY(i / 3) - positions[i + 1]) * influence;
                    z += (target.getZ(i / 3) - positions[i + 2]) * influence;
                  }
                }
              }

              renderList.pushVertex(x, y, z);
            }

            if (attributes.normal !== undefined) {
              const normals = attributes.normal.array;

              for (let i = 0, l = normals.length; i < l; i += 3) {
                renderList.pushNormal(normals[i], normals[i + 1], normals[i + 2]);
              }
            }

            if (attributes.color !== undefined) {
              const colors = attributes.color.array;

              for (let i = 0, l = colors.length; i < l; i += 3) {
                renderList.pushColor(colors[i], colors[i + 1], colors[i + 2]);
              }
            }

            if (attributes.uv !== undefined) {
              const uvs = attributes.uv.array;

              for (let i = 0, l = uvs.length; i < l; i += 2) {
                renderList.pushUv(uvs[i], uvs[i + 1]);
              }
            }

            if (geometry.index !== null) {
              const indices = geometry.index.array;

              if (groups.length > 0) {
                for (let g = 0; g < groups.length; g++) {
                  const group = groups[g];
                  material = isMultiMaterial === true ? object.material[group.materialIndex] : object.material;
                  if (material === undefined) continue;

                  for (let i = group.start, l = group.start + group.count; i < l; i += 3) {
                    renderList.pushTriangle(indices[i], indices[i + 1], indices[i + 2], material);
                  }
                }
              } else {
                for (let i = 0, l = indices.length; i < l; i += 3) {
                  renderList.pushTriangle(indices[i], indices[i + 1], indices[i + 2], material);
                }
              }
            } else {
              if (groups.length > 0) {
                for (let g = 0; g < groups.length; g++) {
                  const group = groups[g];
                  material = isMultiMaterial === true ? object.material[group.materialIndex] : object.material;
                  if (material === undefined) continue;

                  for (let i = group.start, l = group.start + group.count; i < l; i += 3) {
                    renderList.pushTriangle(i, i + 1, i + 2, material);
                  }
                }
              } else {
                for (let i = 0, l = positions.length / 3; i < l; i += 3) {
                  renderList.pushTriangle(i, i + 1, i + 2, material);
                }
              }
            }
          } else if (geometry.isGeometry) {
            console.error('THREE.Projector no longer supports Geometry. Use THREE.BufferGeometry instead.');
            return;
          }
        } else if (object.isLine) {
          _modelViewProjectionMatrix.multiplyMatrices(_viewProjectionMatrix, _modelMatrix);

          if (geometry.isBufferGeometry) {
            const attributes = geometry.attributes;

            if (attributes.position !== undefined) {
              const positions = attributes.position.array;

              for (let i = 0, l = positions.length; i < l; i += 3) {
                renderList.pushVertex(positions[i], positions[i + 1], positions[i + 2]);
              }

              if (attributes.color !== undefined) {
                const colors = attributes.color.array;

                for (let i = 0, l = colors.length; i < l; i += 3) {
                  renderList.pushColor(colors[i], colors[i + 1], colors[i + 2]);
                }
              }

              if (geometry.index !== null) {
                const indices = geometry.index.array;

                for (let i = 0, l = indices.length; i < l; i += 2) {
                  renderList.pushLine(indices[i], indices[i + 1]);
                }
              } else {
                const step = object.isLineSegments ? 2 : 1;

                for (let i = 0, l = positions.length / 3 - 1; i < l; i += step) {
                  renderList.pushLine(i, i + 1);
                }
              }
            }
          } else if (geometry.isGeometry) {
            console.error('THREE.Projector no longer supports Geometry. Use THREE.BufferGeometry instead.');
            return;
          }
        } else if (object.isPoints) {
          _modelViewProjectionMatrix.multiplyMatrices(_viewProjectionMatrix, _modelMatrix);

          if (geometry.isGeometry) {
            console.error('THREE.Projector no longer supports Geometry. Use THREE.BufferGeometry instead.');
            return;
          } else if (geometry.isBufferGeometry) {
            const attributes = geometry.attributes;

            if (attributes.position !== undefined) {
              const positions = attributes.position.array;

              for (let i = 0, l = positions.length; i < l; i += 3) {
                _vector4.set(positions[i], positions[i + 1], positions[i + 2], 1);

                _vector4.applyMatrix4(_modelViewProjectionMatrix);

                pushPoint(_vector4, object, camera);
              }
            }
          }
        } else if (object.isSprite) {
          object.modelViewMatrix.multiplyMatrices(camera.matrixWorldInverse, object.matrixWorld);

          _vector4.set(_modelMatrix.elements[12], _modelMatrix.elements[13], _modelMatrix.elements[14], 1);

          _vector4.applyMatrix4(_viewProjectionMatrix);

          pushPoint(_vector4, object, camera);
        }
      }

      if (sortElements === true) {
        _renderData.elements.sort(painterSort);
      }

      return _renderData;
    };

    function pushPoint(_vector4, object, camera) {
      const invW = 1 / _vector4.w;
      _vector4.z *= invW;

      if (_vector4.z >= -1 && _vector4.z <= 1) {
        _sprite = getNextSpriteInPool();
        _sprite.id = object.id;
        _sprite.x = _vector4.x * invW;
        _sprite.y = _vector4.y * invW;
        _sprite.z = _vector4.z;
        _sprite.renderOrder = object.renderOrder;
        _sprite.object = object;
        _sprite.rotation = object.rotation;
        _sprite.scale.x = object.scale.x * Math.abs(_sprite.x - (_vector4.x + camera.projectionMatrix.elements[0]) / (_vector4.w + camera.projectionMatrix.elements[12]));
        _sprite.scale.y = object.scale.y * Math.abs(_sprite.y - (_vector4.y + camera.projectionMatrix.elements[5]) / (_vector4.w + camera.projectionMatrix.elements[13]));
        _sprite.material = object.material;

        _renderData.elements.push(_sprite);
      }
    } // Pools


    function getNextObjectInPool() {
      if (_objectCount === _objectPoolLength) {
        const object = new RenderableObject();

        _objectPool.push(object);

        _objectPoolLength++;
        _objectCount++;
        return object;
      }

      return _objectPool[_objectCount++];
    }

    function getNextVertexInPool() {
      if (_vertexCount === _vertexPoolLength) {
        const vertex = new RenderableVertex();

        _vertexPool.push(vertex);

        _vertexPoolLength++;
        _vertexCount++;
        return vertex;
      }

      return _vertexPool[_vertexCount++];
    }

    function getNextFaceInPool() {
      if (_faceCount === _facePoolLength) {
        const face = new RenderableFace();

        _facePool.push(face);

        _facePoolLength++;
        _faceCount++;
        return face;
      }

      return _facePool[_faceCount++];
    }

    function getNextLineInPool() {
      if (_lineCount === _linePoolLength) {
        const line = new RenderableLine();

        _linePool.push(line);

        _linePoolLength++;
        _lineCount++;
        return line;
      }

      return _linePool[_lineCount++];
    }

    function getNextSpriteInPool() {
      if (_spriteCount === _spritePoolLength) {
        const sprite = new RenderableSprite();

        _spritePool.push(sprite);

        _spritePoolLength++;
        _spriteCount++;
        return sprite;
      }

      return _spritePool[_spriteCount++];
    } //


    function painterSort(a, b) {
      if (a.renderOrder !== b.renderOrder) {
        return a.renderOrder - b.renderOrder;
      } else if (a.z !== b.z) {
        return b.z - a.z;
      } else if (a.id !== b.id) {
        return a.id - b.id;
      } else {
        return 0;
      }
    }

    function clipLine(s1, s2) {
      let alpha1 = 0,
          alpha2 = 1; // Calculate the boundary coordinate of each vertex for the near and far clip planes,
      // Z = -1 and Z = +1, respectively.

      const bc1near = s1.z + s1.w,
            bc2near = s2.z + s2.w,
            bc1far = -s1.z + s1.w,
            bc2far = -s2.z + s2.w;

      if (bc1near >= 0 && bc2near >= 0 && bc1far >= 0 && bc2far >= 0) {
        // Both vertices lie entirely within all clip planes.
        return true;
      } else if (bc1near < 0 && bc2near < 0 || bc1far < 0 && bc2far < 0) {
        // Both vertices lie entirely outside one of the clip planes.
        return false;
      } else {
        // The line segment spans at least one clip plane.
        if (bc1near < 0) {
          // v1 lies outside the near plane, v2 inside
          alpha1 = Math.max(alpha1, bc1near / (bc1near - bc2near));
        } else if (bc2near < 0) {
          // v2 lies outside the near plane, v1 inside
          alpha2 = Math.min(alpha2, bc1near / (bc1near - bc2near));
        }

        if (bc1far < 0) {
          // v1 lies outside the far plane, v2 inside
          alpha1 = Math.max(alpha1, bc1far / (bc1far - bc2far));
        } else if (bc2far < 0) {
          // v2 lies outside the far plane, v2 inside
          alpha2 = Math.min(alpha2, bc1far / (bc1far - bc2far));
        }

        if (alpha2 < alpha1) {
          // The line segment spans two boundaries, but is outside both of them.
          // (This can't happen when we're only clipping against just near/far but good
          //  to leave the check here for future usage if other clip planes are added.)
          return false;
        } else {
          // Update the s1 and s2 vertices to match the clipped line segment.
          s1.lerp(s2, alpha1);
          s2.lerp(s1, 1 - alpha2);
          return true;
        }
      }
    }
  }

}

THREE.Projector = Projector;
THREE.RenderableFace = RenderableFace;
THREE.RenderableLine = RenderableLine;
THREE.RenderableObject = RenderableObject;
THREE.RenderableSprite = RenderableSprite;
THREE.RenderableVertex = RenderableVertex;
} )();
